<?php

require_once 'Zend/Loader.php';

/**
 * @author Baziak
 * @author Abyr
 */
class Service_Application
{
    private $_basePath = '';

    private $_params = array();
    
    private $_client = null;

    private $_config = null;

    private $_cache = null;

    public function  __construct($config = null, $cache = null)
    {
        if ($config) {
            $this->setConfig($config);
        }
        
        if ($this->getRequestMethod() == 'PUT') {
            $this->_preparePutRequest();
        }

        $this->_params = array_merge($_POST, $_GET);

        $config = $this->_config;

        $frontCache = $config->cache->frontend;
        $backCache = $config->cache->backend;
        $autoSerial = $config->cache->automatic_serialization;
        $cacheTime = $config->cache->frontendOptions->lifetime;
        $cacheDir = $config->cache->backendOptions->cache_dir;

        if (null === $cache) {
            require_once 'Zend/Cache.php';
            $this->_cache = Zend_Cache::factory($frontCache, $backCache,
                array(
                    'caching' => false,
                    'automatic_serialization' => $autoSerial,
                    'lifetime' => $cacheTime
                ), array('cache_dir' => $cacheDir));
        } else {
            $this->_cache = $cache;
        }

        require_once 'Service/Model/CacheableModel.php';
        Service_Model_CacheableModel::setDefaultCache($this->_cache);
    }

    /**
     * @return Service_Response
     * @throws Service_Exception
     */
    public function dispatch()
    {
        try {

            $resource = $this->_getResource($this->_getResourceId());
            
            if (!($resource instanceof Service_Resource)) {
                require_once 'Service/Resource/NotFoundException.php';
                throw new Service_Resource_NotFoundException();
            }

            $resp = $resource->access();

            if (!($resp instanceof Service_Response)) {
                require_once 'Service/Exception.php';
                throw new Service_Exception('System Error: Wrong response "
                    . "from resource.');
            }

        } catch (Service_Resource_HttpMethodNotSupportedException $ex) {
            require_once 'Service/Response/MethodNotAllowed.php';
            require_once 'Log.php';
            Log::writeException($ex, 'method_');
            $resp = new Service_Response_MethodNotAllowed(); // 405
            $resp->setContent('error-message', $ex->getMessage());
            $resp->setContent('error-code', $ex->getCode());
        } catch (Service_Resource_NotFoundException $ex) {
            require_once 'Service/Response/NotFound.php';
            require_once 'Log.php';
            Log::writeException($ex, 'notfound_');
            $resp =  new Service_Response_NotFound(); // 404
            $resp->setContent('error-message', $ex->getMessage());
            $resp->setContent('error-code', $ex->getCode());
        } catch (Service_Authentication_Exception $ex) {
            require_once 'Service/Response/AccessDenied.php';
            require_once 'Log.php';
            Log::writeException($ex, 'auth_');
            $resp = new Service_Response_AccessDenied(); // 403
            $resp->setContent('error-message', $ex->getMessage());
            $resp->setContent('error-code', $ex->getCode());
        } catch (Service_Resource_AccessDeniedException $ex) {
            require_once 'Service/Response/AccessDenied.php';
            require_once 'Log.php';
            Log::writeException($ex, 'access_');
            $resp = new Service_Response_AccessDenied(); // 403
            $resp->setContent('error-message', $ex->getMessage());
            $resp->setContent('error-code', $ex->getCode());
        } catch (Service_Resource_Exception $ex) {
            require_once 'Service/Response/BadRequest.php';
            require_once 'Log.php';
            Log::writeException($ex, 'requests_');
            $resp = new Service_Response_BadRequest(); // 400
            $resp->setContent('error-message', $ex->getMessage());
            $resp->setContent('error-code', $ex->getCode());
        }

        return $resp;
    }

    public function setConfig($config)
    {
        if (is_string($config)) {
            require_once 'Zend/Config/Xml.php';
            $config = new Zend_Config_Xml($config);
        }

        if (!($config instanceof Zend_Config)) {
            require_once 'Service/Exception.php';
            return new Service_Exception();
        }

        $this->_config = $config;

        return $this;
    }

    public function getConfig()
    {
        return $this->_config;
    }
    
    public function setBasePath($basePath)
    {
        if (is_string($basePath)) {
            $this->_basePath = $basePath;
        } else {
            require_once 'Service/Exception.php';
            throw new Service_Exception('Base path should be a string.');
        }
    }

    public function getBasePath()
    {
        return $this->_basePath;
    }

    public function setAcl(Service_Acl $acl)
    {
        $this->_acl = $acl;
    }

    /**
     * Gets an ACL object.
     * 
     * @return Service_Acl
     */
    public function getAcl()
    {
        return $this->_acl;
    }

    public function getHost()
    {
        return $_SERVER["HTTP_HOST"];
    }

    public function getRequestMethod()
    {
        //:TODO is this exception? empty request
        if (isset($_SERVER["REQUEST_METHOD"]))
            return $_SERVER["REQUEST_METHOD"];
        return false;
    }

    public function setRequestMethod($method)
    {
        $_SERVER["REQUEST_METHOD"] = strtoupper($method);
        return true;
    }

    public function getRequestUri()
    {
        return $_SERVER["REQUEST_URI"];
    }

    public function getRequestContentType()
    {
        return $_SERVER['CONTENT_TYPE'];
    }

    public function getRequestContentLength()
    {
        return $_SERVER['CONTENT_LENGTH'];
    }

    public function getRequestContent()
    {
        return file_get_contents("php://input");
    }

    public function getParam($name, $defaultValue = null)
    {
        if (isset($this->_params[$name])) {
            return $this->_params[$name];
        }
        return $defaultValue;
    }

    public function getParams()
    {
        return $this->_params;
    }

    public function setParam($name, $value = null)
    {
        return $this->_params[$name] = $value;
    }
    
    public function setParams($params)
    {
        $this->_params = $params;
        return $this;
    }

    public function getUserAgent()
    {
        return $_SERVER['HTTP_USER_AGENT'];
    }

    /**
     * Gets an identifier of a resource requested by user.
     *
     * @return string
     */
    protected function _getResourceId()
    {
        $uri = $this->getRequestUri();

        // if base path is used
        if ($this->_basePath) {

            //search in url
            $pos = strpos($uri, $this->_basePath);
            if ($pos === 0) {
                $uri = substr($uri, strlen($this->_basePath));
            } else {
                 require_once 'Service/Exception.php';
                 throw new Service_Exception('System Error: request is wrong.');
            }
        }

        $pos = strpos($uri, '?');

        return trim(($pos !== false ? substr($uri, 0, $pos) : $uri), '/');
    }

    /**
     * Prepares $_POST and $_FILES superglobals for PUT request.
     *
     * @todo Refactor this method. It looks too complex.
     */
    protected function _preparePutRequest()
    {
        $contentType = explode(';', $this->getRequestContentType());

        switch (trim($contentType[0])) {
            case 'application/x-www-form-urlencoded':
                $rc = $this->getRequestContent();
                if ( $rc != "" ) {
                    $params = explode('&', $rc);
                    foreach ($params as $key => $param) {
                        if ( $param != "" ) {
                            $param = explode('=', $param);
                            if (!empty($param)) {
                                $param[0] = urldecode($param[0]);
                                $param[1] = isset($param[1]) ?
                                    urldecode($param[1]) : null;

                                // define param in POST
                                $_POST[$param[0]] = $param[1];
                            }
                        }
                    }
                }
                break;
            case 'multipart/form-data':

                $boundary = explode('=', $contentType[1], 2);

                if (empty($boundary) || trim($boundary[0]) != 'boundary') {
                    require_once 'Service/Resource/Exception.php';
                    throw new Service_Resource_Exception(
                        'Multipart Form is wrong: boundary not specified.');
                }
                
                $boundary = "--" . trim($boundary[1], '" ');

                $content = trim($this->getRequestContent());

                $start = strpos($content, "\r\n") + 2;
                $length = strrpos($content, "\r\n") - $start;

                $content = substr($content, $start, $length);

                $parts = explode("\r\n" . $boundary . "\r\n", $content);

                foreach ($parts as $part) {

                    $filename = null;
                    $paramName = null;
                    $contentType = null;
                    $content = null;

                    if ($part && $part != '--') {
                        list($head, $content) = explode("\r\n\r\n", $part);

                        $headers = explode("\r\n", $head);
                        foreach ($headers as $header) {
                            list($name, $value) = explode(':', $header);

                            $name = strtolower(trim($name));

                            if ($name == 'content-type') {
                                $contentType = trim($value);
                            } elseif ($name == 'content-disposition') {
                                foreach (explode(';', $value) as $valuePart) {
                                    $valuePart = explode('=', $valuePart);

                                    if (count($valuePart) == 2) {

                                        switch (trim($valuePart[0])) {
                                            case 'name':
                                                $paramName = trim($valuePart[1],
                                                    '" ');

                                                break;
                                            case 'filename':
                                                $filename = trim($valuePart[1],
                                                    '" ');
                                                break;

                                        }
                                    }
                                }
                            }
                        }

                        if (isset($filename)) {

                            $tmpFile = tempnam($this->_config->tempFolder,
                                'tmp_photo_');

                            file_put_contents($tmpFile, $content);

                            $_FILES[$paramName] = array(
                                'name' => $filename,
                                'type' => $contentType,
                                'tmp_name' => $tmpFile,
                                'error' => 0,
                                'size' => filesize($tmpFile)
                            );

                        } else {
                            $_POST[$paramName] = $content;
                        }
                    }
                }

                break;
        }
    }

    protected function _getRequestedVersion()
    {
        $config = $this->getConfig();

        $defVersion = $config->version->defaultServiceVersion;

        return $this->getParam("_v", $defVersion);
    }

    protected function _getVersionString($version)
    {
        $config = $this->getConfig();

        $defVersion = $config->version->defaultServiceVersion;
        $defVerDelimiter = $config->version->delimiter;
        $versionPrefix = $config->version->prefix;

        if ($version != $defVersion) {
            return $versionPrefix
                    . str_replace(".", $defVerDelimiter, $version);
        }
        return "";
    }

    protected function _getResource($resourceId)
    {
        $resource = null;

        $config = $this->getConfig();

        $defaultVersion = $config->version->defaultServiceVersion;

        //get value from request or default
        $requestedVersion = $this->_getRequestedVersion();

        //use requested version
        $resource = $this->_loadResource($resourceId, $requestedVersion);

        //check for default
        if (!$resource && $requestedVersion != $defaultVersion) {
            //use default version
            $resource = $this->_loadResource($resourceId, $defaultVersion);
        }

        return $resource;
    }

    protected function _loadResource($resourceId, $version)
    {
        $config = $this->getConfig();

        $parts = explode($config->resources->packageSeparator, $resourceId);
        foreach ($parts as &$part) {

            $words = explode($config->resources->wordsSeparator, $part);
            
            if (count($words)) {
                $words = array_map('ucfirst', $words);
            
                $part = implode('', $words);
            }

            $part = ucfirst($part);
        }
        $config = $this->getConfig();
        $path = realpath($config->resources->directory) . DIRECTORY_SEPARATOR
              . implode(DIRECTORY_SEPARATOR, $parts)
              . $this->_getVersionString($version)
              . $config->resources->postfix;

        if (Zend_Loader::isReadable($path)) {
            include_once $path;
            $className = implode('_', $parts) 
                . $this->_getVersionString($version);
            return new $className($this, $resourceId);
        }

        //resource not found
        return false;
    }

}
